﻿using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.ServiceModel;
using System.Text.RegularExpressions;
using System.Web.UI;
using BrandonHaynes.Membership.Factors.PromptControls;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Users;
using DotNetNuke.Security.Membership;
using DotNetNuke.Services.Log.EventLog;
using DotNetNuke.Services.Mail;
using System;

namespace BrandonHaynes.Membership.Factors
	{
	/// <summary>
	/// A base class for sending a one-time password sent to a user via a SMS gateway service.
	/// 
	/// Note that this class may be extended to support most any SMS service gateway.
	/// </summary>
	public abstract class AbstractSmsFactor : IAuthenticationFactor
		{
		/// <summary>
		/// A set of attributes as specified in the web.config 
		/// </summary>
		protected IDictionary<string, string> Attributes { get; private set; }

		public AbstractSmsFactor(IDictionary<string, string> attributes)
			{ Attributes = attributes; }

		#region IAuthenticationFactor Members

		public abstract string Name { get; }

		public virtual void Authenticate(UserInfo user, Credential credential)
			{
			var telephone = user.GetUserProperty(Attributes.GetValueOrDefault("profileProperty", "Cell"));

			// If no telephone is specified, the user will not be authenticated
			if (!string.IsNullOrEmpty(telephone))
				{
				// We (in a small kludge) persist our one-time password in the MembershipUser.Comments property.
				// This property is not used by the DotNetNuke system, but it is also not secured against access.
				//		Probably should strengthen this -- perhaps via encryption or persistence in the profile.
				var aspnetUser = System.Web.Security.Membership.GetUser(user.Username);
				var onetimePassword = aspnetUser.Comment;

				if (credential[Name] == onetimePassword && !string.IsNullOrEmpty(credential[Name]))
					{
					// The user has supplied the one-time password at credential[Name].  Proceed to authenticate.
					// Also, clear the one-time password.
					aspnetUser.Comment = string.Empty;
					System.Web.Security.Membership.UpdateUser(aspnetUser);
					}
				else
					{
					// No one-time password was submitted, or it does not match the one generated.
					// Accordingly, create a new one...
					aspnetUser.Comment = System.Web.Security.Membership.GeneratePassword(5, 0);
					System.Web.Security.Membership.UpdateUser(aspnetUser);

					// Send it...
					SendSms(user, FormatTelephoneByCountry(user, telephone), aspnetUser.Comment);

					// And mark the credential as incomplete.
					credential.IncompleteFactors.Add(this);
					}

				// Proceed with additional authentication (however, the credential might be incomplete as above)
				credential.Status = UserLoginStatus.LOGIN_SUCCESS;
				}
			else
				// No email address is bad news.  The user can't ever authenticate.
				// I'd love to just go ahead and authenticate when no e-mail address exists, but this leads to
				// attacks that are preferrable to just avoid.
				credential.Status = UserLoginStatus.LOGIN_USERLOCKEDOUT;
			}

		/// <summary>
		/// Retrieves a control used to collect credential information (in this case a prompt for the one-time password)
		/// </summary>
		public virtual Control PromptControl
			{ get { return new PasswordPrompt(Name, Attributes); } }

		#endregion

		/// <summary>
		/// Transmits a one-time password to a user via the SMS gateway service
		/// </summary>
		/// <param name="user">The user attempting to authenticate</param>
		/// <param name="telephone">The destination telephone number for the SMS message</param>
		/// <param name="onetimePassword">The one-time password to be used for subsequent authentication</param>
		protected abstract void SendSms(UserInfo user, string telephone, string onetimePassword);

		/// <summary>
		/// The world's simplest (and probably poorest) E.164 telephone formatting utility.
		/// 
		/// Supports only the +1/United States format at this time, but can be extended to support
		/// any nationality (sorry guys!).  Note that although it does not format other nationalities well,
		/// a telephone value that is _already_ properly formatted will continue to function correctly.
		/// 
		/// Anyone donation of code that can well-format an arbitrary E.164 value would be greatly 
		/// appreciated!
		/// </summary>
		/// <param name="user">The user to format against (which by default looks at country, then
		/// preferredLocale, and then the portal's default language</param>
		/// <param name="telephone">The telephone value to format</param>
		/// <returns>A formatted telephone value for use at the SMS gateway</returns>
		protected virtual string FormatTelephoneByCountry(UserInfo user, string telephone)
			{
			telephone = Regex.Replace(telephone, @"[^\d\+]", string.Empty);

			switch (user.Profile.Country ??
				user.Profile.PreferredLocale ??
				PortalController.GetCurrentPortalSettings().DefaultLanguage)
				{
				case "United States":
				case "US":
				case "en-US":
					if (!telephone.StartsWith("1", StringComparison.OrdinalIgnoreCase))
						telephone = string.Concat('1', telephone);
					break;
				}

			return string.Concat('+', telephone);
			}
		}
	}
